package com.xxxx.comment.service.impl;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxxx.comment.constants.RedisConstants;
import com.xxxx.comment.constants.httpcode.HttpConstants;
import com.xxxx.comment.entity.User;
import com.xxxx.comment.entity.dto.LoginFormDTO;
import com.xxxx.comment.entity.dto.Result;
import com.xxxx.comment.entity.dto.UserDTO;
import com.xxxx.comment.mapper.UserMapper;
import com.xxxx.comment.service.IUserService;
import com.xxxx.comment.utils.BeanCopyUtils;
import com.xxxx.comment.utils.RegexUtils;
import com.xxxx.comment.utils.SystemConstants;
import com.xxxx.comment.utils.UserHolder;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 用户服务实现类
 *
 * @author lzf
 * @date 2023/7/9 18:25:09
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService
{
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session)
    {
        // 校验手机号
        if (RegexUtils.isPhoneInvalid(phone))
            return Result.fail("手机号格式错误！");
        // 生成验证码
        String code = RandomUtil.randomNumbers(6);
//        // 保存验证码到session
//        session.setAttribute(phone, code);
        // 保存验证码到redis
        stringRedisTemplate.opsForValue()
                .set(RedisConstants.LOGIN_CODE_KEY_PRE + phone, code,
                        RedisConstants.LOGIN_CODE_TTL, TimeUnit.SECONDS);
        // 发送验证码
        log.debug("发送短信验证码成功，验证码为{}，该验证码有效期为5分钟", code);

        return Result.ok(code);
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session)
    {
        if (RegexUtils.isPhoneInvalid(loginForm.getPhone()))
            return Result.fail("手机号格式错误！");
        // 从redis中获取验证码
        String loginKey = RedisConstants.LOGIN_CODE_KEY_PRE + loginForm.getPhone();
        String code = stringRedisTemplate.opsForValue().get(loginKey);
        if (code == null)
            return Result.fail("验证码不存在，请发送验证码！");
        if (!code.equals(loginForm.getCode()))
            return Result.fail("验证码错误！");
        // 登录成功，删除验证码
        stringRedisTemplate.delete(loginKey);

        // 查询用户信息，没有用户信息（未注册）则自动注册
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone, loginForm.getPhone());
        User user = getOne(queryWrapper);
        if (user == null)
            user = createUserWithPhone(loginForm.getPhone());
        // 保存用户信息到redis
        // 随机生成token，作为登录令牌
        String token = UUID.randomUUID().toString();
        // 将 User 转为 Hash 保存
        UserDTO userDTO = BeanCopyUtils.copyBean(user, UserDTO.class);
        userDTO.setToken(token);
        // BeanUtil.beanToMap 中，默认情况下 Long 类型不能转为 String 类型、
        // 所以需要在 beanToMap(userDto, ...) 中添加一大堆处理，手动把所有类型的value转为String类型
        Map<String, Object> beanToMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((field, value) -> value.toString()));
        // 存储
        stringRedisTemplate.opsForHash()
                .putAll(RedisConstants.LOGIN_USER_KEY + token, beanToMap);
        // 设置 token 有效期为一天
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,
                RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        return Result.ok(token);
    }

    @Override
    public Result logout() {
        String token = UserHolder.getUser().getToken();
        if (!Objects.nonNull(token))
            return Result.fail("用户未登录或重复注销！");
        Boolean deleted = stringRedisTemplate.delete(RedisConstants.LOGIN_USER_KEY + token);
        return Result.ok(Boolean.TRUE.equals(deleted) ? "注销登录成功！" : "注销登录失败，请联系管理员");
    }

    @Override
    public Result sign() {
        UserDTO user = UserHolder.getUser();
        // 1、获取当前日期，同时根据年、月，构建key的后缀
        LocalDateTime now = LocalDateTime.now();
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        // 2、拼接key
        String key = RedisConstants.USER_SIGN_KEY + user.getId() + keySuffix;
        // 3、判断今天是本月的第几天
        int today = now.getDayOfMonth();
        // 4、写入Redis，SETBIT key offset 1。为什么减一，因为redis中的BItMap位图也是从0开始，而天数是从1开始的
        // 当然，不减也可以，直接用天数对应位置的bit进行存储，不过会浪费一个bit（第0位）的内存，尽管不多，但签到的用户多了就比较可观了，尽管1000W用户敲到也才1M内存[doge]
        // 在Redis中，签到了第一天、第二天、第九天的bitmap：1100000010000000，从前后往后是第一天到第31天
        Boolean signResult = stringRedisTemplate.opsForValue().setBit(key, today - 1, true);
        // SETBIT sign:1:202310 0 1，会返回 offset位置上原来的值(0或1)
        // 而bitmap创建后所有位置上默认都是0，所以SETBIT返回0，则0：代表签到成功，1：代表重复签到
        if (Boolean.FALSE.equals(signResult))
            return Result.ok();
        else
            return Result.fail(HttpConstants.HAD_SIGNED);
    }

    @Override
    public Result signCount() {
        // 1、获取当前登录用户
        UserDTO user = UserHolder.getUser();
        // 2、获取当前日期，同时根据年、月，构建key的后缀
        LocalDateTime now = LocalDateTime.now();
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        // 3、拼接key
        String key = RedisConstants.USER_SIGN_KEY + user.getId() + keySuffix;
        // 4、判断今天是本月的第几天
        int today = now.getDayOfMonth();
        // 5、BITFIELD sign:1010:202310 GET u10 0，从sign:1010:202310中第零位开始往后十位
        // 获取本月截止今天为止的所有的签到记录，返回的是一个十进制的数字
        // 构建 BITFIELD 子命令 u10
        BitFieldSubCommands.BitFieldType bitFieldType = BitFieldSubCommands.BitFieldType.unsigned(today);
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                .get(bitFieldType).valueAt(0);
        // 执行 BITFIELD 命令
        List<Long> signBit = stringRedisTemplate.opsForValue().bitField(key, bitFieldSubCommands);
        if (signBit == null || CollectionUtils.isEmpty(signBit))
            return Result.ok(0); // 没有任何签到结果
        Long num = signBit.get(0);
        if (num == null || num == 0)
            return Result.ok(0);
        // 6、循环遍历
        int count = 0;
        // 为0，说明未签到，结束统计
        while ((num & 1) != 0) {
            // 6.1、这个数字与1做与运算，得到数字的最后一个bit位 ，& 1 就是最后一位与1进行“或”运算，判断对应的天数是否签到
            count++;
            // 把数字右移一位，抛弃最后一个bit位，继续下一个bit位
            num >>>= 1;
        }

        return Result.ok(count);
    }

    /**
     * 使用手机号注册账号
     *
     * @param phone 手机号
     * @return 注册的用户信息
     */
    public User createUserWithPhone(String phone)
    {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        save(user);

        return user;
    }
}
